using UnityEngine;
using Math;
using Plane=Math.Plane;

namespace Terrain
{
    public static class ClipMap
    {
        public static void Create(int n, out LevelData data)
        {
            data                        = new LevelData();
            data.NumLevels              = 3;
            data.MinLevel               = 0;
            data.LevelPosCurrent               = new IVector2[data.NumLevels];
            data.LevelPosPrevious       = new IVector2[data.NumLevels];
            data.LevelOffset            = new IVector2[data.NumLevels];

            GetLevelOffsets(n, data.NumLevels, data.LevelOffset);
        }

        public static void Create(int n, LevelData levels, out BlockData data)
        {
            data                        = new BlockData();
            data.M                      = (n+1)/4;
            data.N                      = n;
            data.NumBlocksFinest        = 4;
            data.NumBlocksPerRing       = 12;
            data.BlockHeight            = (n+1)/8;
            data.BlockOrigin            = new IVector2[data.NumBlocksPerRing];
            data.BlockOriginFinest      = new IVector2[data.NumBlocksFinest];
            data.Blocks                 = new BlockData.Block[(levels.NumLevels*data.NumBlocksPerRing)+data.NumBlocksFinest+levels.NumLevels];
            data.BlocksVisible          = new int[levels.NumLevels];
            data.BlocksVisibleFinest    = 0;

            GetBlockOrigins(data.M, data.BlockOrigin);
            GetBlockOriginsFinest(data.M, data.BlockOriginFinest);
        }

        public static void CreateQuad(int m, out Vector3[] vertices, out int[] indices)
        {
            int vertexCount = m*m;
            int indexCount = (m-1)*(m-1)*6;

            vertices = new Vector3[vertexCount];
            indices = new int[indexCount];

            for (int z=0; z<m; ++z)
            {
                for (int x=0; x<m; ++x)
                {
                    vertices[z*m+x].x = (float)z;
                    vertices[z*m+x].z = (float)x;
                    vertices[z*m+x].y = 0.0f;
                }
            }

            int index = 0;

            for (int z=0; z<m-1; ++z)
            {
                for (int x=0; x<m-1; ++x)
                {
                    indices[index++] = z*m+x;
                    indices[index++] = z*m+(x+1);
                    indices[index++] = (z+1)*m+(x+1);
                    indices[index++] = (z+1)*m+x;
                    indices[index++] = z*m+x;
                    indices[index++] = (z+1)*m+(x+1);
                }
            }
        }

        public static void CreateRect(int m, int n, out Vector3[] vertices, out int[] indices)
        {
            int vertexCount = m*12;
            int indexCount = ((m-1)*2)*24;

            vertices = new Vector3[vertexCount];
            indices = new int[indexCount];

            int index = 0;

            for (int b=0; b<4; ++b)
            {
                int offset = b*(3*m);

                if ((b&1)==0)
                {
                    for (int z=0; z<2; ++z)
                    {
                        for (int x=0; x<m-1; ++x)
                        {
                            indices[index++] = offset+x*3+z;
                            indices[index++] = offset+x*3+(z+1);
                            indices[index++] = offset+(x+1)*3+(z+1);
                            indices[index++] = offset+(x+1)*3+z;
                            indices[index++] = offset+x*3+z;
                            indices[index++] = offset+(x+1)*3+(z+1);
                        }
                    }
                }
                else
                {
                    for (int z=0; z<2; ++z)
                    {
                        for (int x=0; x<m-1; ++x)
                        {
                            indices[index++] = offset+(x+1)*3+z;
                            indices[index++] = offset+x*3+(z+1);
                            indices[index++] = offset+(x+1)*3+(z+1);
                            indices[index++] = offset+(x+1)*3+z;
                            indices[index++] = offset+x*3+z;
                            indices[index++] = offset+x*3+(z+1);
                        }
                    }
                }
            }

            int vertex = 0;

            for (int z=0; z<m; ++z)
            {
                for (int x=(2*m-2); x<(2*m)+1; x++)
                {
                    vertices[vertex].x = (float)((z*n+x)/n);
                    vertices[vertex].z = (float)((z*n+x)-((z*n+x)/n)*n);
                    vertices[vertex].y = 0.0f;
                    vertex++;
                }
            }

            for (int z=0; z<m; ++z)
            {
                for (int x=(2*m); x>=(2*m-2); x--)
                {
                    vertices[vertex].x = (float)((x*n+z)/n);
                    vertices[vertex].z = (float)((x*n+z)-((x*n+z)/n)*n);
                    vertices[vertex].y = 0.0f;
                    vertex++;
                }
            }

            for (int z=(n-1)-(m-1); z<n; ++z)
            {
                for (int x=(2*m-2); x<(2*m)+1; ++x)
                {
                    vertices[vertex].x = (float)((z*n+x)/n);
                    vertices[vertex].z = (float)((z*n+x)-((z*n+x)/n)*n);
                    vertices[vertex].y = 0.0f;
                    vertex++;
                }
            }

            for (int z=(n-1)-(m-1); z<n; ++z)
            {
                for (int x=(2*m); x>=(2*m-2); --x)
                {
                    vertices[vertex].x = (float)((x*n+z)/n);
                    vertices[vertex].z = (float)((x*n+z)-((x*n+z)/n)*n);
                    vertices[vertex].y = 0.0f;
                    vertex++;
                }
            }
        }

        public static void CreateRing(int m, int n, out Vector3[] vertices, out int[] indices)
        {
            int vertexCount = (n-1)*4;
            int indexCount = (n-2)*6;

            vertices = new Vector3[vertexCount];
            indices = new int[indexCount];

            int index = 0;

            for (int i=0; i<(n-2)*4; i+=2)
            {
                indices[index++] = i;
                indices[index++] = i+2;
                indices[index++] = i+1;
            }

            int vertex = 0;

            for (int i=0; i<n; ++i)
            {
                vertices[vertex].x = i/n;
                vertices[vertex].z = i-vertices[vertex].x*n;
                vertex++;
            }

            for (int i=1; i<n-1; ++i)
            {
                vertices[vertex].x = (i*n+(n-1))/n;
                vertices[vertex].z = (i*n+(n-1))-vertices[vertex].x*n;
                vertex++;
            }

            for (int i=(n-1); i>=0; --i)
            {
                vertices[vertex].x = ((n-1)*n+i)/n;
                vertices[vertex].z = ((n-1)*n+i)-vertices[vertex].x*n;
                vertex++;
            }

            for (int i=(n-2); i>=1; i--)
            {
                vertices[vertex].x = (i*n)/n;
                vertices[vertex].z = (i*n)-vertices[vertex].x*n;
                vertex++;
            }
        }

        public static void CreateL0(int m, int n, out Vector3[] vertices, out int[] indices)
        {
            int vertexCount = (m*8)+2;
            int indexCount = ((m*8)-2)*3;

            vertices = new Vector3[vertexCount];
            indices = new int[indexCount];

            int index = 0;

            for (int i=0; i<(m*4); i+=2)
            {
                indices[index++] = i;
                indices[index++] = i+1;
                indices[index++] = i+2;
                indices[index++] = i+1;
                indices[index++] = i+3;
                indices[index++] = i+2;
            }

            for (int i=(m*4)+2; i<(m*8); i+=2)
            {
                indices[index++] = i;
                indices[index++] = i+1;
                indices[index++] = i+3;
                indices[index++] = i;
                indices[index++] = i+3;
                indices[index++] = i+2;
            }

            int vertex = 0;

            for (int z=(m-1); z<(n-(m-1)); ++z)
            {
                for (int x=m; x>=(m-1); --x)
                {
                    int v = x*n+z;
                    vertices[vertex].x = v/n;
                    vertices[vertex].z = v-vertices[vertex].x*n;
                    vertex++;
                }
            }

            for (int x=m; x<(n-1)-(m-2); ++x)
            {
                for (int z=((n-1)-m); z<(n-1)-m+2; ++z)
                {
                    int v = x*n+z;
                    vertices[vertex].x = v/n;
                    vertices[vertex].z = v-vertices[vertex].x*n;
                    vertex++;
                }
            }
        }

        public static void CreateL1(int m, int n, out Vector3[] vertices, out int[] indices)
        {
            int vertexCount = (m*8)+2;
            int indexCount = ((m*8)-2)*3;

            vertices = new Vector3[vertexCount];
            indices = new int[indexCount];

            int index = 0;

            for (int i=0; i<(m*4); i+=2)
            {
                indices[index++] = i;
                indices[index++] = i+1;
                indices[index++] = i+2;
                indices[index++] = i+1;
                indices[index++] = i+3;
                indices[index++] = i+2;
            }

            for (int i=(m*4)+2; i<(m*8); i+=2)
            {
                indices[index++] = i;
                indices[index++] = i+1;
                indices[index++] = i+3;
                indices[index++] = i;
                indices[index++] = i+3;
                indices[index++] = i+2;
            }

            int vertex = 0;

            for (int x=((n-1)-(m-2)-1); x>(m-2); --x)
            {
                for (int z=((n-1)-m); z<(n-1)-(m-1)+1; ++z)
                {
                    int v = z*n+x;
                    vertices[vertex].x = v/n;
                    vertices[vertex].z = v-vertices[vertex].x*n;
                    vertex++;
                }
            }

            for (int x=(n-(m+1)); x>(m-2); --x)
            {
                for (int z=((n-1)-m+1); z>(n-1)-m-1; --z)
                {
                    int v = x*n+z;
                    vertices[vertex].x = v/n;
                    vertices[vertex].z = v-vertices[vertex].x*n;
                    vertex++;
                }
            }
        }

        public static void CreateL2(int m, int n, out Vector3[] vertices, out int[] indices)
        {
            int vertexCount = (m*8)+2;
            int indexCount = ((m*8)-2)*3;

            vertices = new Vector3[vertexCount];
            indices = new int[indexCount];

            int index = 0;

            for (int i=0; i<(m*4); i+=2)
            {
                indices[index++] = i;
                indices[index++] = i+1;
                indices[index++] = i+2;
                indices[index++] = i+1;
                indices[index++] = i+3;
                indices[index++] = i+2;
            }

            for (int i=(m*4)+2; i<(m*8); i+=2)
            {
                indices[index++] = i;
                indices[index++] = i+1;
                indices[index++] = i+3;
                indices[index++] = i;
                indices[index++] = i+3;
                indices[index++] = i+2;
            }

            int vertex = 0;

            for (int x=((n-1)-(m-1)); x>(m-2); --x)
            {
                for (int z=((n-1)-m); z<(n-1)-(m-1)+1; ++z)
                {
                    int v = z*n+x;
                    vertices[vertex].x = v/n;
                    vertices[vertex].z = v-vertices[vertex].x*n;
                    vertex++;
                }
            }

            for (int z=(n-(m+1)); z>(m-2); --z)
            {
                for (int x=m; x>(m-2); --x)
                {
                    int v = z*n+x;
                    vertices[vertex].x = v/n;
                    vertices[vertex].z = v-vertices[vertex].x*n;
                    vertex++;
                }
            }
        }

        public static void CreateL3(int m, int n, out Vector3[] vertices, out int[] indices)
        {
            int vertexCount = (m*8)+2;
            int indexCount = ((m*8)-2)*3;

            vertices = new Vector3[vertexCount];
            indices = new int[indexCount];

            int index = 0;

            for (int i=0; i<(m*4); i+=2)
            {
                indices[index++] = i;
                indices[index++] = i+1;
                indices[index++] = i+2;
                indices[index++] = i+1;
                indices[index++] = i+3;
                indices[index++] = i+2;
            }

            for (int i=(m*4)+2; i<(m*8); i+=2)
            {
                indices[index++] = i;
                indices[index++] = i+1;
                indices[index++] = i+3;
                indices[index++] = i;
                indices[index++] = i+3;
                indices[index++] = i+2;
            }

            int vertex = 0;

            for (int z=(m-1); z<n-(m-1); ++z)
            {
                for (int x=m; x>(m-2); --x)
                {
                    int v = x*n+z;
                    vertices[vertex].x = v/n;
                    vertices[vertex].z = v-vertices[vertex].x*n;
                    vertex++;
                }
            }

            for (int z=(n-m); z>(m-1); --z)
            {
                for (int x=m; x>(m-2); --x)
                {
                    int v = z*n+x;
                    vertices[vertex].x = v/n;
                    vertices[vertex].z = v-vertices[vertex].x*n;
                    vertex++;
                }
            }
        }

        public static void GetBlockBounds(int m, int level, IVector2 levelOffset, IVector2 levelPosition, IVector2 blockOrigin, int blockHeight, ref Box box)
        {
            int blockScale = 1<<level;
            int blockWidth = (m-1)*blockScale;

            box.min.x = levelOffset.x+levelPosition.x+blockOrigin.x*blockScale;
            box.min.z = levelOffset.y+levelPosition.y+blockOrigin.y*blockScale;
            box.min.y = -blockHeight;

            box.max.x = box.min.x+blockWidth;
            box.max.z = box.min.z+blockWidth;
            box.max.y = blockHeight;
        }

        public static void GetBlockOrigins(int m, IVector2[] blocks)
        {
            blocks[0] = new IVector2(0, 0);
            blocks[1] = new IVector2(0, m-1);
            blocks[2] = new IVector2(0, 2*m);
            blocks[3] = new IVector2(0, 3*m-1);

            blocks[4] = new IVector2(3*m-1, 0);
            blocks[5] = new IVector2(3*m-1, m-1);
            blocks[6] = new IVector2(3*m-1, 2*m);
            blocks[7] = new IVector2(3*m-1, 3*m-1);

            blocks[8] = new IVector2(m-1, 3*m-1);
            blocks[9] = new IVector2(m-1, 0);

            blocks[10] = new IVector2(2*m, 3*m-1);
            blocks[11] = new IVector2(2*m, 0);
        }

        public static void GetBlockOriginsFinest(int m, IVector2[] blocks)
        {
            blocks[0] = new IVector2(m, m);
            blocks[1] = new IVector2(m, 2*m-1);
            blocks[2] = new IVector2(2*m-1, m);
            blocks[3] = new IVector2(2*m-1, 2*m-1);
        }

        public static void GetLevelOffsets(int n, int levels, IVector2[] levelOffsets)
        {
            for (int i=0, offset = 0; i<levels; offset-=(1<<i++))
            {
                levelOffsets[i].x = (-(n/2)<<i)-offset;
                levelOffsets[i].y = (-(n/2)<<i)-offset;
            }
        }

        public static void MovePosition(ref IVector2 currPos, Vector3 newPos, out IVector2 moveDir, LevelData levels, BlockData data)
        {
            int dx = Maths.Floor(newPos.x)-currPos.x;
            int dy = Maths.Floor(newPos.z)-currPos.y;

            currPos.x += dx;
            currPos.y += dy;

            moveDir.x = Maths.Clamp(dx, -1, 1);
            moveDir.y = Maths.Clamp(dy, -1, 1);

            if (moveDir.x!=0 || moveDir.y!=0)
            {
                for (int level=levels.MinLevel; level<levels.NumLevels; ++level)
                {
                    levels.LevelPosPrevious[level] = levels.LevelPosCurrent[level];
                    levels.LevelPosCurrent[level].x = currPos.x&((~0)<<(level+1));
                    levels.LevelPosCurrent[level].y = currPos.y&((~0)<<(level+1));
                }
            }
        }

        public static void UpdateHeightMaps(IVector2 currPos, IVector2 moveDir, LevelData levels, BlockData data, HeightData heights)
        {
            if (moveDir.x==0 && moveDir.y==0)
                return;

            for (int level=0; level<levels.NumLevels; level++)
            {
                if (levels.LevelPosPrevious[level]==levels.LevelPosCurrent[level])
                    continue;

                IVector2 from = default(IVector2);
                IVector2 to = default(IVector2);

                from.x = levels.LevelPosPrevious[level].x>>level;
                from.y = levels.LevelPosPrevious[level].y>>level;
                to.x = levels.LevelPosCurrent[level].x>>level;
                to.y = levels.LevelPosCurrent[level].y>>level;

                HeightMap.UpdateHeights(from, to, heights.TexSize, level, levels.LevelOffset[level], heights.HeightMaps[level]);
            }
        }

        public static void CullBlocks(Plane[] frustum, LevelData levels, BlockData data, HeightData heights)
        {
            int index = levels.MinLevel*12;

            // Cull the 12 ring-blocks
            for (int level=levels.MinLevel; level<levels.NumLevels; ++level)
            {
                data.BlocksVisible[level] = 0;
                for (int i=0; i<data.NumBlocksPerRing; i++)
                    data.BlocksVisible[level] += CullBlock(frustum, level, data.BlockOrigin[i], ref index, levels, data, heights)?1:0;
                index += 12-data.BlocksVisible[level];
            }

            // Cull the 4 finest blocks
            data.BlocksVisibleFinest = 0;
            for (int i=0; i<data.NumBlocksFinest; i++)
                data.BlocksVisibleFinest += CullBlock(frustum, levels.MinLevel, data.BlockOriginFinest[i], ref index, levels, data, heights)?1:0;

            // LShape & Mx3 blocks
            index += (4-data.BlocksVisibleFinest);
            index += levels.MinLevel;

            for (int level=levels.MinLevel; level<levels.NumLevels; ++level)
            {
                data.Blocks[index].WorldPosAndScale.x = levels.LevelPosCurrent[level].x+levels.LevelOffset[level].x;
                data.Blocks[index].WorldPosAndScale.y = levels.LevelPosCurrent[level].y+levels.LevelOffset[level].y;
                data.Blocks[index].WorldPosAndScale.z = (1<<level);
                data.Blocks[index].WorldPosAndScale.w = (1<<level);

                data.Blocks[index].TexturePosAndScale.x = heights.OneOverTexSize*(levels.LevelPosCurrent[level].x>>level);
                data.Blocks[index].TexturePosAndScale.y = heights.OneOverTexSize*(levels.LevelPosCurrent[level].y>>level);
                data.Blocks[index].TexturePosAndScale.z = heights.OneOverTexSize;
                data.Blocks[index].TexturePosAndScale.w = heights.OneOverTexSize;

                index++;
            }
        }

        private static bool CullBlock(Plane[] frustum, int level, IVector2 blockOrigin, ref int index, LevelData levels, BlockData data, HeightData heights)
        {
            Box bounds = default(Box);
            ClipMap.GetBlockBounds(data.M, level, levels.LevelOffset[level], levels.LevelPosCurrent[level], blockOrigin, data.BlockHeight, ref bounds);

            if (Intersect.BoxInFrustum(frustum, bounds.min, bounds.max))
            {
                data.Blocks[index].WorldPosAndScale.x = bounds.min.x;
                data.Blocks[index].WorldPosAndScale.y = bounds.min.z;
                data.Blocks[index].WorldPosAndScale.z = (1<<level);
                data.Blocks[index].WorldPosAndScale.w = (1<<level);

                data.Blocks[index].TexturePosAndScale.x = heights.OneOverTexSize*(blockOrigin.x+(levels.LevelPosCurrent[level].x>>level));
                data.Blocks[index].TexturePosAndScale.y = heights.OneOverTexSize*(blockOrigin.y+(levels.LevelPosCurrent[level].y>>level));
                data.Blocks[index].TexturePosAndScale.z = heights.OneOverTexSize;
                data.Blocks[index].TexturePosAndScale.w = heights.OneOverTexSize;

                index++;

                return true;
            }
            return false;
        }
    }
}